package net.esj.basic.core.engine.util;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.net.URL;
import java.net.URLDecoder;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarInputStream;

import net.esj.basic.core.engine.tools.test.ClassTest;
import net.esj.basic.core.engine.tools.test.ResourceTest;
import net.esj.basic.core.engine.tools.test.Test;
import net.esj.basic.core.engine.tools.test.inst.ClassAnnotatedWith;
import net.esj.basic.core.engine.tools.test.inst.ClassIsAssignableFrom;
import net.esj.basic.core.engine.tools.test.inst.ResourceNameIs;

import org.apache.log4j.Logger;

public class ResolverUtils {
	
	private static final Logger LOG = Logger.getLogger(ResolverUtils.class);
	
	private static ClassLoader classLoader;

	public static Set findNamedResource(String name, String... pathNames) {
        if (pathNames == null) return Collections.EMPTY_SET;
        
        Test test = new ResourceNameIs(name);
        Set set = new HashSet();
        for (String pkg : pathNames) {
        	set.addAll(findInPackage(test, pkg));
        }
        return set;
    }
	
	public static Set findImplementations(Class parent, String... packageNames) {
        if (packageNames == null) return Collections.EMPTY_SET;

        Test test = new ClassIsAssignableFrom(parent);
        Set set = new HashSet();
        for (String pkg : packageNames) {
            set.addAll(findInPackage(test, pkg));
        }
        return set;
    }
	
	public static Set findAnnotated(Class<? extends Annotation> annotation, String... packageNames) {
        if (packageNames == null) return Collections.EMPTY_SET;

        Test test = new ClassAnnotatedWith(annotation);
        Set set = new HashSet();
        for (String pkg : packageNames) {
            set.addAll(findInPackage(test, pkg));
        }
        return set;
    }
	
	public static ClassLoader getClassLoader() {
        return classLoader == null ? Thread.currentThread().getContextClassLoader() : classLoader;
    }
	
	public static Set findInPackage(Test test, String packageName) {
        packageName = packageName.replace('.', '/');
        ClassLoader loader = getClassLoader();
        Enumeration<URL> urls;

        try {
            urls = loader.getResources(packageName);
        }
        catch (IOException ioe) {
            LOG.warn("Could not read package: " + packageName, ioe);
            return null;
        }

        Set set = new HashSet();
        
        while (urls.hasMoreElements()) {
            try {
                String urlPath = urls.nextElement().getFile();
                urlPath = URLDecoder.decode(urlPath, "UTF-8");

                // If it's a file in a directory, trim the stupid file: spec
                if ( urlPath.startsWith("file:") ) {
                    urlPath = urlPath.substring(5);
                }

                // Else it's in a JAR, grab the path to the jar
                if (urlPath.indexOf('!') > 0) {
                    urlPath = urlPath.substring(0, urlPath.indexOf('!'));
                }

                LOG.debug("Scanning for classes in [" + urlPath + "] matching criteria: " + test);
                File file = new File(urlPath);
                if ( file.isDirectory() ) {
                    loadImplementationsInDirectory(test, packageName, file,set);
                }
                else {
                    loadImplementationsInJar(test, packageName, file,set);
                }
            }
            catch (IOException ioe) {
                LOG.warn("could not read entries", ioe);
            }
        }
        return set;
    }
	
	private static void loadImplementationsInDirectory(Test test, String parent, File location,Set set) {
        File[] files = location.listFiles();
        StringBuilder builder = null;

        for (File file : files) {
            builder = new StringBuilder(100);
            builder.append(parent).append("/").append(file.getName());
            String packageOrClass = ( parent == null ? file.getName() : builder.toString() );

            if (file.isDirectory()) {
                loadImplementationsInDirectory(test, packageOrClass, file,set);
            }
            else {
                addIfMatching(test, packageOrClass,set);
            }
        }
    }
	
	private static void loadImplementationsInJar(Test test, String parent, File jarfile,Set set) {

        try {
            JarEntry entry;
            JarInputStream jarStream = new JarInputStream(new FileInputStream(jarfile));

            while ( (entry = jarStream.getNextJarEntry() ) != null) {
                String name = entry.getName();
                if (!entry.isDirectory() && name.startsWith(parent)) {
                    addIfMatching(test, name,set);
                }
            }
        }
        catch (IOException ioe) {
            LOG.error("Could not search jar file '" + jarfile + "' for classes matching criteria: " +
                      test + " due to an IOException", ioe);
        }
    }
	
	protected static void addIfMatching(Test test, String fqn,Set set) {
        try {
            ClassLoader loader = getClassLoader();
            if (test instanceof ClassTest) {
                String externalName = fqn.substring(0, fqn.indexOf('.')).replace('/', '.');
                if (LOG.isDebugEnabled()) {
                    LOG.debug("Checking to see if class " + externalName + " matches criteria [" + test + "]");
                }
    
                Class type = loader.loadClass(externalName);
                if (test.test(type) ) {
                    set.add( type);
                }
            }
            if (test instanceof ResourceTest) {
                URL url = loader.getResource(fqn);
                if (url == null) {
                    url = loader.getResource(fqn.substring(1));
                }
                if (url != null && test.test(url)) {
                    set.add(url);
                }
            }
        }
        catch (Throwable t) {
            LOG.warn("Could not examine class '" + fqn + "' due to a " +
                     t.getClass().getName() + " with message: " + t.getMessage());
        }
    }

	public void setClassLoader(ClassLoader classLoader) {
		this.classLoader = classLoader;
	}
	
	public static void main(String[] args){
		//Set<URL> set = ResolverUtils.findNamedResource("spring.handlers", "META-INF/spring.handlers");
		Enumeration urls;
		try {
			urls = getClassLoader().getResources("META-INF/spring.handlers");
			while (urls.hasMoreElements()) {
				URL url = (URL) urls.nextElement();
				System.out.println(url.getPath());
			}
		} catch (IOException e) {
			e.printStackTrace();
		}
		//Set<URL> set = ResolverUtils.findNamedResource("pom.properties", "slf4j-log4j12-1.5.6.jar!/META-INF/maven");
//		for(URL url : set){
//			System.out.println(url.getPath());
//		}
	}
}
